feat: sync Client + sync Middleware/Retry/Bulkhead + RetryBudget thread-safety#31
Merged
Conversation
Sync Client (landing in this PR) may share a Retry/RetryBudget across a ThreadPoolExecutor. The token-bucket deques would race without a lock. Uncontended lock cost in CPython is ~50-100ns per op — negligible vs HTTP latency. Existing async paths are unaffected; lock is taken unconditionally for one type, one mental model, no flag at the call site.
…on_mapping.py Shared by AsyncClient (and the upcoming Client). Pure function; the existing _httpx2_exception_mapper context manager in client.py will delegate to this in Task B5.
…to _internal/status.py Both worlds share the status-code dispatch (already pure sync) and the STREAMING_BODY_MARKER. Predicates split: _is_streaming_body_async checks __aiter__; _is_streaming_body_sync checks __iter__ with list/tuple in the safe-list (common in sync code, never streaming).
…helpers Pulls map_httpx2_exception, _raise_on_status_error, _is_streaming_body_*, and STREAMING_BODY_MARKER out of client.py into _internal/. Behavior unchanged; sets up Client (sync) to share the same dispatch.
Same algorithm: budget deposit per attempt, status/network/timeout gates, idempotent-method check, streaming-body refusal, Retry-After, full-jitter backoff, budget refusal, PEP-678 add_note on giving up. Uses time.sleep for the delay; no attempt_timeout (removed from both worlds in PR 1). Shares the same observability emitters (httpware.retry logger, retry.streaming_refused / retry.giving_up / retry.budget_refused events).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTTP methods land in the next commit; stream() after. Reword _HTTPX2_CLIENT_CONFLICT_MESSAGE and _DEFAULT_DECODER_MISSING_MESSAGE to be world-neutral since they are now shared between Client and AsyncClient. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/head/options/request/send) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ decorators Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ClientmatchingAsyncClient(typed decoding, middleware,Retry,Bulkhead,stream(),with/close()lifecycle,httpx2.Clientinjection).Middleware,Next,before_request/after_response/on_errordecorators, synccompose.Retry(usestime.sleep) and syncBulkhead(usesthreading.Semaphore).RetryBudgetthread-safe via an internalthreading.Lock. Same class for both worlds; a single instance is safe to share across (syncClient,AsyncClient) pairs and across threads.map_httpx2_exception,_raise_on_status_error, streaming-body predicates,STREAMING_BODY_MARKER) to_internal/exception_mapping.pyand_internal/status.py.Part 2 of 2. Part 1 (rename, PR #30) is already merged. Cut one combined release after this lands.
Source spec:
planning/specs/2026-06-07-sync-client-design.md. Plan:planning/plans/2026-06-07-sync-client-plan.md(Tasks B1–B25). Release notes drafted atplanning/releases/0.8.0.md.Test plan
just lintcleanjust testall green — 371 tests, 100% line coveragetest_client_sync.py,test_retry_sync.py,test_bulkhead_sync.py,test_middleware_sync.py,test_client_stream_sync.pytest_retry_budget_threadsafety.pyproves the lock keeps the deques consistent under concurrent deposit/withdrawtest_threading_with_shared_budget.pyproves a singleRetryBudgetworks under mixed sync-threads + async-loop pressuretest_optional_extras_isolation.pystill green — sync addition introduces no new transitive importsmkdocs build --strictcleanRelease notes
Draft at
planning/releases/0.8.0.md. The version decision (0.8.0vs1.0.0) can be made at tag time.🤖 Generated with Claude Code